/******************************************************************************* * Copyright (c) 2000, 2015 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation * Dan Rubel (dan_rubel@instantiations.com) - accessor to get menu id * Lars Vogel <Lars.Vogel@vogella.com> - Bug 472654 *******************************************************************************/ package org.eclipse.ui.internal; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.core.runtime.IConfigurationElement; import org.eclipse.core.runtime.IExtensionDelta; import org.eclipse.core.runtime.IExtensionPoint; import org.eclipse.core.runtime.IRegistryChangeEvent; import org.eclipse.core.runtime.IRegistryChangeListener; import org.eclipse.core.runtime.Platform; import org.eclipse.e4.core.contexts.IEclipseContext; import org.eclipse.e4.ui.internal.workbench.ContributionsAnalyzer; import org.eclipse.e4.ui.internal.workbench.OpaqueElementUtil; import org.eclipse.e4.ui.internal.workbench.swt.AbstractPartRenderer; import org.eclipse.e4.ui.internal.workbench.swt.MenuService; import org.eclipse.e4.ui.model.application.ui.MElementContainer; import org.eclipse.e4.ui.model.application.ui.MUIElement; import org.eclipse.e4.ui.model.application.ui.basic.MPart; import org.eclipse.e4.ui.model.application.ui.menu.MMenu; import org.eclipse.e4.ui.model.application.ui.menu.MMenuElement; import org.eclipse.e4.ui.model.application.ui.menu.MPopupMenu; import org.eclipse.e4.ui.model.application.ui.menu.impl.MenuFactoryImpl; import org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer; import org.eclipse.e4.ui.workbench.swt.factories.IRendererFactory; import org.eclipse.jface.action.IContributionItem; import org.eclipse.jface.action.IMenuListener2; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.action.Separator; import org.eclipse.jface.action.SubMenuManager; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.ISelectionProvider; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.IWorkbenchPartSite; import org.eclipse.ui.internal.registry.IWorkbenchRegistryConstants; /** * This class extends a single popup menu */ public class PopupMenuExtender implements IMenuListener2, IRegistryChangeListener { /** * The bit in <code>bitSet</code> that stores whether the static actions * have been read from the registry. */ private static final int STATIC_ACTION_READ = 1; /** * The bit in <code>bitSet</code> that stores whether the editor input * should be included for the sake of object contributions. */ private static final int INCLUDE_EDITOR_INPUT = 1 << 1; private final MenuManager menu; private SubMenuManager menuWrapper; private final ISelectionProvider selProvider; private final IWorkbenchPart part; private Map<String, ViewerActionBuilder> staticActionBuilders = null; /** * The boolean properties maintained by this extender. A bit set is used to * save memory. */ private int bitSet = 0; private ArrayList<PluginActionContributionItem> actionContributionCache = new ArrayList<>(); private boolean cleanupNeeded = false; private MPart modelPart; /** * The context that will be used to create the popup menu's context under. */ private IEclipseContext context; /** * Construct a new menu extender. * * @param id * the menu id * @param menu * the menu to extend * @param prov * the selection provider * @param part * the part to extend * @param context * the context to create the child popup menu context under */ public PopupMenuExtender(String id, MenuManager menu, ISelectionProvider prov, IWorkbenchPart part, IEclipseContext context) { this(id, menu, prov, part, context, true); } /** * Construct a new menu extender. * * @param id * the menu id * @param menu * the menu to extend * @param prov * the selection provider * @param part * the part to extend * @param context * the context to create the child popup menu context under * @param includeEditorInput * Whether the editor input should be included when adding object * contributions to this context menu. */ public PopupMenuExtender(final String id, final MenuManager menu, final ISelectionProvider prov, final IWorkbenchPart part, IEclipseContext context, final boolean includeEditorInput) { super(); this.menu = menu; this.selProvider = prov; this.part = part; this.context = context; this.modelPart = part.getSite().getService(MPart.class); if (includeEditorInput) { bitSet |= INCLUDE_EDITOR_INPUT; } menu.addMenuListener(this); if (!menu.getRemoveAllWhenShown()) { menuWrapper = new SubMenuManager(menu); menuWrapper.setVisible(true); } createModelFor(id); addMenuId(id); Platform.getExtensionRegistry().addRegistryChangeListener(this); } private void createModelFor(String id) { if (id == null) { id = getClass().getName() + '.' + System.identityHashCode(this); } menuModel = null; for (MMenu item : modelPart.getMenus()) { if (id.equals(item.getElementId()) && item instanceof MPopupMenu && item.getTags().contains("popup")) { //$NON-NLS-1$ menuModel = (MPopupMenu) item; break; } } if (menuModel == null) { menuModel = MenuFactoryImpl.eINSTANCE.createPopupMenu(); menuModel.setElementId(id); menuModel.getTags().add(ContributionsAnalyzer.MC_POPUP); modelPart.getMenus().add(menuModel); } IRendererFactory factory = modelPart.getContext().get(IRendererFactory.class); AbstractPartRenderer obj = factory.getRenderer(menuModel, null); if (obj instanceof MenuManagerRenderer) { ((MenuManagerRenderer) obj).linkModelToManager(menuModel, menu); } registerE4Support(); cleanUpContributionCache(); } private void registerE4Support() { if (menuModel.getWidget() == null && menu.getMenu() != null) { MenuService.registerMenu(menu.getMenu().getParent(), menuModel, context); } } // getMenuId() added by Dan Rubel (dan_rubel@instantiations.com) /** * Return the menu identifiers for this extender. * * @return The set of all identifiers that represent this extender. */ public Set<String> getMenuIds() { if (staticActionBuilders == null) { return Collections.emptySet(); } return staticActionBuilders.keySet(); } /** * <p> * Adds another menu identifier to this extender. An extender can represent * many menu identifiers. These identifiers should represent the same menu * manager, selection provider and part. Duplicate identifiers are * automatically ignored. * </p> * <p> * For example, it is necessary to filter out duplicate identifiers for * <code>CompilationUnitEditor</code> instances, as these define both * <code>"#CompilationUnitEditorContext"</code> and * <code>"org.eclipse.jdt.ui.CompilationUnitEditor.EditorContext"</code> * as menu identifier for the same pop-up menu. We don't want to contribute * duplicate items in this case. * </p> * * @param menuId * The menu identifier to add to this extender; should not be * <code>null</code>. */ public final void addMenuId(final String menuId) { bitSet &= ~STATIC_ACTION_READ; if (menuModel != null) { List<String> tags = menuModel.getTags(); String tag = "popup:" + menuId; //$NON-NLS-1$ if (!tags.contains(tag)) { tags.add(tag); } } readStaticActionsFor(menuId); } /** * Determines whether this extender would be the same as another extender * created with the given values. Two extenders are equivalent if they have * the same menu manager, selection provider and part (i.e., if the menu * they represent is about to show, they would populate it with duplicate * values). * * @param menuManager * The menu manager with which to compare; may be * <code>null</code>. * @param selectionProvider * The selection provider with which to compare; may be * <code>null</code>. * @param part * The part with which to compare; may be <code>null</code>. * @return <code>true</code> if the menu manager, selection provider and * part are all the same. */ public final boolean matches(final MenuManager menuManager, final ISelectionProvider selectionProvider, final IWorkbenchPart part) { return (this.menu == menuManager) && (this.selProvider == selectionProvider) && (this.part == part); } /** * Contributes items registered for the currently active editor. */ private void addEditorActions(IMenuManager mgr, Set<IObjectActionContributor> alreadyContributed) { ISelectionProvider activeEditor = new ISelectionProvider() { @Override public void addSelectionChangedListener( ISelectionChangedListener listener) { throw new UnsupportedOperationException( "This ISelectionProvider is static, and cannot be modified."); //$NON-NLS-1$ } @Override public ISelection getSelection() { if (part instanceof IEditorPart) { final IEditorPart editorPart = (IEditorPart) part; return new StructuredSelection(new Object[] { editorPart .getEditorInput() }); } return new StructuredSelection(new Object[0]); } @Override public void removeSelectionChangedListener( ISelectionChangedListener listener) { throw new UnsupportedOperationException( "This ISelectionProvider is static, and cannot be modified."); //$NON-NLS-1$ } @Override public void setSelection(ISelection selection) { throw new UnsupportedOperationException( "This ISelectionProvider is static, and cannot be modified."); //$NON-NLS-1$ } }; if (ObjectActionContributorManager.getManager().contributeObjectActions(part, mgr, activeEditor, alreadyContributed)) { mgr.add(new Separator()); } } /** * Contributes items registered for the object type(s) in * the current selection. */ private void addObjectActions(IMenuManager mgr, Set<IObjectActionContributor> alreadyContributed) { if (selProvider != null) { if (ObjectActionContributorManager.getManager().contributeObjectActions(part, mgr, selProvider, alreadyContributed)) { mgr.add(new Separator()); } } } /** * Disposes all of the static actions. */ private final void clearStaticActions() { bitSet &= ~STATIC_ACTION_READ; if (staticActionBuilders != null) { final Iterator<ViewerActionBuilder> staticActionBuilderItr = staticActionBuilders .values().iterator(); while (staticActionBuilderItr.hasNext()) { final ViewerActionBuilder staticActionBuilder = staticActionBuilderItr.next(); staticActionBuilder.dispose(); } } } /** * Adds static items to the context menu. */ private void addStaticActions(IMenuManager mgr) { if (staticActionBuilders != null) { final Iterator<ViewerActionBuilder> staticActionBuilderItr = staticActionBuilders .values().iterator(); while (staticActionBuilderItr.hasNext()) { final ViewerActionBuilder staticActionBuilder = staticActionBuilderItr.next(); staticActionBuilder.contribute(mgr, null, true); } } } /** * Notifies the listener that the menu is about to be shown. */ @Override public void menuAboutToShow(IMenuManager mgr) { registerE4Support(); // Add this menu as a visible menu. final IWorkbenchPartSite site = part.getSite(); if (site != null) { final IWorkbench workbench = site.getWorkbenchWindow() .getWorkbench(); if (workbench instanceof Workbench) { final Workbench realWorkbench = (Workbench) workbench; runCleanUp(realWorkbench); ISelection input = null; if ((bitSet & INCLUDE_EDITOR_INPUT) != 0) { if (part instanceof IEditorPart) { final IEditorPart editorPart = (IEditorPart) part; input = new StructuredSelection( new Object[] { editorPart.getEditorInput() }); } } ISelection s = (selProvider == null ? null : selProvider .getSelection()); realWorkbench.addShowingMenus(getMenuIds(), s, input); } } addMenuContributions(mgr); readStaticActions(); // test for additions removed to comply with menu contributions if (menuWrapper != null) { mgr = menuWrapper; menuWrapper.removeAll(); } Set<IObjectActionContributor> contributedItems = new HashSet<>(); if ((bitSet & INCLUDE_EDITOR_INPUT) != 0) { addEditorActions(mgr, contributedItems); } addObjectActions(mgr, contributedItems); addStaticActions(mgr); } /** * well, this goes to the renderer. * * @param mgr */ private void addMenuContributions(IMenuManager mgr) { IRendererFactory factory = modelPart.getContext().get(IRendererFactory.class); AbstractPartRenderer obj = factory.getRenderer(menuModel, null); if (obj instanceof MenuManagerRenderer) { MenuManagerRenderer renderer = (MenuManagerRenderer) obj; renderer.reconcileManagerToModel(menu, menuModel); renderer.processContributions(menuModel, menuModel.getElementId(), false, true); // double cast because we're bad people renderer.processContents((MElementContainer<MUIElement>) ((Object) menuModel)); } } private MPopupMenu menuModel; /** * Notifies the listener that the menu is about to be hidden. */ @Override public final void menuAboutToHide(final IMenuManager mgr) { gatherContributions(mgr); cleanupNeeded = true; // Remove this menu as a visible menu. final IWorkbenchPartSite site = part.getSite(); if (site != null) { final IWorkbench workbench = site.getWorkbenchWindow().getWorkbench(); if (workbench instanceof Workbench) { // try delaying this until after the selection event // has been fired. // This is less threatening if the popup: menu // contributions aren't tied to the evaluation service workbench.getDisplay().asyncExec(() -> { final Workbench realWorkbench = (Workbench) workbench; runCleanUp(realWorkbench); }); } } } private void runCleanUp(Workbench realWorkbench) { if (!cleanupNeeded) { return; } cleanupNeeded = false; realWorkbench.removeShowingMenus(getMenuIds(), null, null); cleanUpContributionCache(); } private void gatherContributions(final IMenuManager mgr) { final IContributionItem[] items = mgr.getItems(); for (IContributionItem item : items) { if (item instanceof PluginActionContributionItem) { actionContributionCache.add((PluginActionContributionItem) item); } else if (item instanceof IMenuManager) { gatherContributions(((IMenuManager) item)); } } } private void cleanUpContributionCache() { if (!actionContributionCache.isEmpty()) { PluginActionContributionItem[] items = actionContributionCache .toArray(new PluginActionContributionItem[actionContributionCache.size()]); actionContributionCache.clear(); for (PluginActionContributionItem item : items) { item.dispose(); } } if (modelPart == null || menuModel == null) { return; } IEclipseContext modelContext = modelPart.getContext(); if (modelContext != null) { IRendererFactory factory = modelContext.get(IRendererFactory.class); if (factory != null) { AbstractPartRenderer obj = factory.getRenderer(menuModel, null); if (obj instanceof MenuManagerRenderer) { MenuManagerRenderer renderer = (MenuManagerRenderer) obj; renderer.cleanUp(menuModel); } } } } /** * Read all of the static items for the content menu. */ private final void readStaticActions() { if (staticActionBuilders != null) { final Iterator<String> menuIdItr = staticActionBuilders.keySet().iterator(); while (menuIdItr.hasNext()) { final String menuId = menuIdItr.next(); readStaticActionsFor(menuId); } } } /** * Read static items for a particular menu id, into the context menu. */ private void readStaticActionsFor(final String menuId) { if ((bitSet & STATIC_ACTION_READ) != 0) { return; } bitSet |= STATIC_ACTION_READ; // If no menu id provided, then there is no contributions // to add. Fix for bug #33140. if ((menuId == null) || (menuId.length() < 1)) { return; } if (staticActionBuilders == null) { staticActionBuilders = new HashMap<>(); } Object object = staticActionBuilders.get(menuId); if (!(object instanceof ViewerActionBuilder)) { object = new ViewerActionBuilder(); staticActionBuilders.put(menuId, (ViewerActionBuilder) object); } final ViewerActionBuilder staticActionBuilder = (ViewerActionBuilder) object; staticActionBuilder.readViewerContributions(menuId, selProvider, part); } /** * Dispose of the menu extender. Should only be called when the part * is disposed. */ public void dispose() { clearStaticActions(); Platform.getExtensionRegistry().removeRegistryChangeListener(this); menu.removeMenuListener(this); if (menuModel != null) { // unlink ourselves from the renderer IRendererFactory factory = modelPart.getContext().get(IRendererFactory.class); AbstractPartRenderer obj = factory.getRenderer(menuModel, null); if (obj instanceof MenuManagerRenderer) { MenuManagerRenderer renderer = (MenuManagerRenderer) obj; unlink(renderer, menuModel); renderer.clearModelToManager(menuModel, menu); } modelPart.getMenus().remove(menuModel); } } /** * Unlink all contribution items from the given model menu. * * @param renderer * the renderer that is holding the links * @param menu * the model menu whose children should have its items unlinked * from their corresponding contribution items */ private void unlink(MenuManagerRenderer renderer, MMenu menu) { for (MMenuElement menuElement : menu.getChildren()) { if (OpaqueElementUtil.isOpaqueMenuItem(menuElement) || OpaqueElementUtil.isOpaqueMenuSeparator(menuElement)) { Object item = OpaqueElementUtil.getOpaqueItem(menuElement); if (item instanceof IContributionItem) { renderer.clearModelToContribution(menuElement, (IContributionItem) item); OpaqueElementUtil.clearOpaqueItem(menuElement); } } else if (menuElement instanceof MMenu) { MMenu subMenu = (MMenu) menuElement; unlink(renderer, subMenu); MenuManager manager = renderer.getManager(subMenu); if (manager != null) { renderer.clearModelToManager(subMenu, manager); } } else { IContributionItem contribution = renderer.getContribution(menuElement); if (contribution != null) { renderer.clearModelToContribution(menuElement, contribution); } } } } @Override public void registryChanged(final IRegistryChangeEvent event) { Display display = Display.getDefault(); if (part != null) { display = part.getSite().getPage().getWorkbenchWindow().getWorkbench().getDisplay(); } //check the delta to see if there are any viewer contribution changes. if so, null our builder to cause reparsing on the next menu show IExtensionDelta [] deltas = event.getExtensionDeltas(); for (IExtensionDelta delta : deltas) { IExtensionPoint extensionPoint = delta.getExtensionPoint(); if (extensionPoint.getContributor().getName().equals(WorkbenchPlugin.PI_WORKBENCH) && extensionPoint.getSimpleIdentifier().equals( IWorkbenchRegistryConstants.PL_POPUP_MENU)) { boolean clearPopups = false; IConfigurationElement [] elements = delta.getExtension().getConfigurationElements(); for (IConfigurationElement element : elements) { if (element.getName().equals(IWorkbenchRegistryConstants.TAG_VIEWER_CONTRIBUTION)) { clearPopups = true; break; } } if (clearPopups) { display.syncExec(() -> clearStaticActions()); } } } } public MenuManager getManager() { return menu; } }